/**
 * Copyright © 2011 Creativor Studio.
 * About license information, please see LICENSE.txt.
 */
package org.creativor.rayson.transport.common;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * An abstract object which is used to read packet out from one socket channel.
 * 
 * @author Nick Zhang
 */
public abstract class PacketReader
{
	/**
	 * A indicator is used to test whether there is a http request header
	 * message in the data buffer.<br/>
	 * It holds for lights. Each light associated with a byte as following
	 * table. When doing a matching process, for lights on indicate that all
	 * lights match it's associated byte. Which means a new http request header
	 * is found.
	 * 
	 * <pre>
	 * +-------+-------+--------+-------+-------+
	 * | light |   1   |	2	|	3	|	4	|
	 * +-------+-------+--------+-------+-------+
	 * | byte  |   CR  |	LF	|	CR	|	LF	|
	 * +-------+-------+--------+-------+-------+
	 * </pre>
	 * 
	 * @see Packet#CR
	 * @see Packet#LF
	 * @author Nick Zhang
	 */
	private class Indicator
	{
		private int onLightIndex;
		private int position;
		
		public Indicator()
		{
			reset();
		}
		
		public int getPosition()
		{
			return position;
		}
		
		public void reset()
		{
			onLightIndex = -1;
			position = 0;
		}
		
		/**
		 * Do a match progress.
		 * 
		 * @return True if for lights all match it's associated byte so that
		 *         they all turned on. Or else, false.
		 */
		public boolean match()
		{
			while (position + onLightIndex < buffer.position())
			{
				if (LIGHTS[onLightIndex + 1].match(buffer.get(position + onLightIndex + 1)))
				{
					onLightIndex++;
					if (onLightIndex == LIGHT_COUNT - 1) return true;
					
				} else
				{
					position++;
					onLightIndex = -1;
				}
			}
			return false;
		}
		
	}
	
	/**
	 * A light associated with a byte. Once a compared byte is matched with this
	 * light's byte, this light will be turn on.
	 * 
	 * @author Nick Zhang
	 */
	private static class Light
	{
		private final byte b;
		
		Light(byte b)
		{
			this.b = b;
		}
		
		/**
		 * @return True if the compare byte equals to this light's byte.
		 */
		boolean match(byte compare)
		{
			return compare == b;
		}
	}
	
	private enum State
	{
		DATA, HEADER, NEW;
	}
	
	private static final int LIGHT_COUNT = 4;
	/**
	 * Static four lights. Shared in all the packet readers.
	 */
	private static final Light[] LIGHTS = new Light[LIGHT_COUNT];
	private static final int PACKET_BUFFER_LENGTH = 1024 * 40;
	
	static
	{
		LIGHTS[0] = new Light(Packet.CR);
		LIGHTS[1] = new Light(Packet.LF);
		LIGHTS[2] = new Light(Packet.CR);
		LIGHTS[3] = new Light(Packet.LF);
	}
	private ByteBuffer buffer;
	private ByteBuffer dataBuffer;
	private Indicator indicator;
	private Packet currentPacket;
	private final SocketChannel socketChannel;
	private State state;
	
	/**
	 * Instantiates a new packet reader.
	 * 
	 * @param socketChannel
	 *            the socket channel
	 */
	public PacketReader(final SocketChannel socketChannel)
	{
		this.socketChannel = socketChannel;
		
		buffer = ByteBuffer.allocate(PACKET_BUFFER_LENGTH);
		indicator = new Indicator();
		state = State.NEW;
	}
	
	/**
	 * Checks if is ready.
	 * 
	 * @return true, if is ready
	 */
	public boolean isReady()
	{
		// TODO:
		return false;// return this.state == State.READY;
	}
	
	/**
	 * Read.
	 * 
	 * @return the int
	 * @throws IOException
	 *             Signals that an I/O exception has occurred.
	 * @throws ChecksumMatchException
	 *             the checksum match exception
	 * @throws PacketException
	 *             the packet exception
	 */
	public int read() throws IOException, PacketException
	{
		int readCount = this.socketChannel.read(this.buffer);
		readPackets();
		return readCount;
	}
	
	/**
	 * @throws PacketException
	 */
	private void readPackets() throws PacketException
	{
		switch (this.state)
		{
			case NEW:
				System.out.println("previous packet:" + this.currentPacket);
				this.currentPacket = new Packet();
				this.state = State.HEADER;
			case HEADER:
			{
				boolean bContinue = false;
				if (indicator.match())
				{
					int position = indicator.getPosition() + LIGHT_COUNT - 1;
					byte[] requestHeader = new byte[position + 1];
					int oldPosition = buffer.position();
					int newPosition = oldPosition - position;
					buffer.position(0);
					buffer.get(requestHeader);
					buffer.compact();
					buffer.position(newPosition);
					indicator.reset();
					// set packet header
					this.currentPacket.setRequestHeader(requestHeader);
					int packetDataLength = this.currentPacket.getDataLength();
					if (packetDataLength > 0)
					{
						this.dataBuffer = ByteBuffer.allocate(packetDataLength);
						this.state = State.DATA;
						bContinue = true;
					} else
					{
						this.state = State.NEW;
						readPackets();
					}
				}
				if (!bContinue) break;
			}
			case DATA:
			{
				int transferCount = 0;
				if (this.buffer.position() >= this.dataBuffer.remaining())
					transferCount = this.dataBuffer.remaining();
				else if (!this.buffer.hasRemaining())
				{
					if (this.dataBuffer.remaining() > this.buffer.capacity())
						transferCount = this.buffer.capacity();
					else
					{
						transferCount = this.dataBuffer.remaining();
					}
				}
				if (transferCount > 0)
				{
					this.buffer.flip();
					// transfer data.
					while (transferCount > 0)
					{
						this.dataBuffer.put(this.buffer.get());
						transferCount--;
					}
					this.buffer.compact();// Position always set to zero.
					if (!this.dataBuffer.hasRemaining())
					{
						this.currentPacket.setData(this.dataBuffer.array());
						this.dataBuffer = null;
						this.state = State.NEW;
						readPackets();
					}
				}
				break;
			}
				
			default:
				break;
		}
	}
	
	/**
	 * Take last with type.
	 * 
	 * @return the packet with type
	 */
	public PacketWithType takeLastWithType()
	{
		if (!isReady()) throw new IllegalStateException("Read stat must be ready");
		this.state = State.HEADER;
		return null;// new PacketWithType(lastCode, lastPacket);
	}
}
